Spring Boot MVC 项目最佳实践规范
本文档整理了项目开发中的核心规范和常用代码模板,方便编码时快速查阅。
一、命名规范
| 类型 |
命名格式 |
示例 |
| Entity |
XxxEntity 或 Xxx |
User、Team |
| DTO |
XxxDTO 或 XxxRequest |
UserRegisterDTO |
| VO |
XxxVO |
UserVO |
| Enum |
XxxEnum |
GenderEnum |
| Service |
XxxService |
UserService |
| ServiceImpl |
XxxServiceImpl |
UserServiceImpl |
| Controller |
XxxController |
UserController |
| Mapper |
XxxMapper |
UserMapper |
二、分层架构
┌─────────────┐
│ Controller │ → 参数接收、非空校验、触发DTO校验、调用Service
├─────────────┤
│ Service │ → 业务逻辑、业务校验、事务管理、调用Mapper
├─────────────┤
│ Mapper │ → 数据库操作(CRUD)
├─────────────┤
│ Entity │ → 数据库表映射
├─────────────┤
│ DTO │ → 请求数据封装、字段校验
├─────────────┤
│ VO │ → 响应数据封装、脱敏处理
└─────────────┘
职责边界:
- Controller:只做参数接收和结果返回,不写业务逻辑
- Service:核心业务逻辑,事务控制在此层
- Mapper:纯数据库操作,不含业务判断
三、校验注解速查
| 注解 |
作用 |
示例 |
@NotNull |
不能为 null |
@NotNull Long id |
@NotBlank |
字符串非空且非空白 |
@NotBlank String name |
@NotEmpty |
集合/数组非空 |
@NotEmpty List<String> tags |
@Size |
长度/大小范围 |
@Size(min=1, max=10) |
@Min / @Max |
数值范围 |
@Min(0) @Max(100) |
@Email |
邮箱格式 |
@Email String email |
@Pattern |
正则匹配 |
@Pattern(regexp="...") |
@AssertTrue |
自定义校验方法 |
@AssertTrue isValid() |
@Valid |
触发嵌套校验 |
@Valid AddressDTO address |
@Validated |
分组校验 |
@Validated(UpdateGroup.class) |
使用示例:
@Data
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20字符")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度6-20字符")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
}
四、枚举设计规范
4.1 标准枚举模板
@Getter
@AllArgsConstructor
public enum GenderEnum {
MALE(0, "男"),
FEMALE(1, "女"),
UNKNOWN(2, "未知");
@EnumValue
private final Integer code;
@JsonValue
private final String desc;
@JsonCreator
public static GenderEnum fromCode(Integer code) {
if (code == null) return null;
for (GenderEnum e : values()) {
if (e.code.equals(code)) return e;
}
return null;
}
public static boolean isValid(Integer code) {
return fromCode(code) != null;
}
}
4.2 枚举设计要点
| 注解 |
作用 |
@EnumValue |
标记存储到数据库的字段 |
@JsonValue |
标记序列化到 JSON 的字段 |
@JsonCreator |
标记反序列化的工厂方法 |
必备方法:
fromCode() - 根据 code 获取枚举
isValid() - 验证 code 是否有效
五、异常处理规范
5.1 异常使用原则
| 做法 |
说明 |
| ✅ 定义专有 ErrorCode |
为不同业务错误定义不同的错误码 |
| ✅ 添加上下文信息 |
抛出异常时提供具体信息 |
| ✅ 使用 ThrowUtils |
简化异常抛出代码 |
| ✅ 区分错误类型 |
CLIENT(用户错误) vs SERVER(系统错误) |
| ✅ 提前校验 |
在关键操作前进行参数和业务校验 |
5.2 正确示例
throw new BusinessException(
ErrorCode.USER_NOT_FOUND,
"User with id " + userId + " does not exist"
);
ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);
ThrowUtils.throwIf(amount <= 0, ErrorCode.PARAMS_ERROR, "金额必须大于0");
throw new BusinessException(ErrorCode.ERROR);
5.3 ThrowUtils 常用方法
ThrowUtils.throwIf(condition, ErrorCode.XXX);
ThrowUtils.throwIf(condition, ErrorCode.XXX, "详细信息");
ThrowUtils.throwIfNull(obj, ErrorCode.XXX);
User user = ThrowUtils.throwIfNull(userMapper.selectById(id), ErrorCode.USER_NOT_FOUND);
ThrowUtils.throwIfBlank(str, ErrorCode.XXX);
六、权限校验
6.1 注解方式
| 注解 |
作用 |
示例 |
@Anonymous |
允许匿名访问 |
@Anonymous @GetMapping("/public") |
@RequiresPermission("xxx") |
需要单个权限 |
@RequiresPermission("user:add") |
@RequiresPermission(value={"a","b"}, logical=Logical.OR) |
任一权限即可 |
满足 a 或 b |
@RequiresPermission(value={"a","b"}, logical=Logical.AND) |
需要所有权限 |
同时有 a 和 b |
@RequiresRole("xxx") |
需要角色 |
@RequiresRole("admin") |
6.2 获取用户信息
LoginUserVO user = SecurityUtils.getLoginUser();
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
LoginUserVO user = SecurityUtils.requireLoginUser();
Long userId = SecurityUtils.requireUserId();
6.3 权限校验(失败抛异常)
SecurityUtils.checkPermission("user:add");
SecurityUtils.checkPermission("user:add", "user:edit");
SecurityUtils.checkAnyPermission("user:add", "user:edit");
SecurityUtils.checkRole("admin");
SecurityUtils.checkAnyRole("admin", "manager");
6.4 权限判断(返回 boolean)
boolean has = SecurityUtils.hasPermission("user:add");
boolean hasAny = SecurityUtils.hasAnyPermission("user:add", "user:edit");
boolean isAdmin = SecurityUtils.isAdmin();
boolean isSuperAdmin = SecurityUtils.isSuperAdmin();
boolean isLoggedIn = SecurityUtils.isAuthenticated();
6.5 数据级权限
SecurityUtils.checkSelfOrAdmin(userId);
SecurityUtils.checkOwner(ownerId);
boolean can = SecurityUtils.isSelfOrAdmin(userId);
6.6 认证状态
SecurityUtils.requireAuthenticated();
SecurityUtils.requireNotAuthenticated();
七、配置管理
7.1 配置层级
优先级从低到高:
application.yml → application-{profile}.yml → .env.{profile}
7.2 环境变量映射规则
YAML 配置 → 环境变量
app.name → APP_NAME
app.jwt.secret → APP_JWT_SECRET
app.file.max-size → APP_FILE_MAX_SIZE
7.3 注入 AppProperties
| 方式 |
代码 |
适用场景 |
| 构造器注入(推荐) |
public MyService(AppProperties appProperties) |
Service、Controller |
| 字段注入 |
@Autowired private AppProperties appProperties; |
简单场景 |
| 方法参数 |
public void method(AppProperties config) |
特殊场景 |
@Service
public class AuthService {
private final AppProperties appProperties;
public AuthService(AppProperties appProperties) {
this.appProperties = appProperties;
}
}
7.4 配置项速查表
| 获取方式 |
返回类型 |
说明 |
appProperties.getName() |
String |
应用名称 |
appProperties.getVersion() |
String |
应用版本 |
appProperties.isDebug() |
boolean |
是否调试模式 |
appProperties.isProduction() |
boolean |
是否生产环境 |
appProperties.isDevelopment() |
boolean |
是否开发环境 |
JWT 配置 appProperties.getJwt()
| 方法 |
返回类型 |
说明 |
.getSecret() |
String |
JWT 密钥 |
.getExpiration() |
long |
过期时间(秒) |
.getExpirationMs() |
long |
过期时间(毫秒) |
.getTokenPrefix() |
String |
Token 前缀,默认 Bearer |
.getHeaderName() |
String |
Header 名称,默认 Authorization |
安全配置 appProperties.getSecurity()
| 方法 |
返回类型 |
说明 |
.getPasswordSalt() |
String |
密码静态盐值 |
.getBcryptStrength() |
int |
BCrypt 强度(4-31) |
文件配置 appProperties.getFile()
| 方法 |
返回类型 |
说明 |
.getMaxSize() |
long |
最大文件大小(字节) |
.getMaxSizeMB() |
long |
最大文件大小(MB) |
.getAllowedFormats() |
String |
允许格式(逗号分隔) |
.getAllowedFormatArray() |
String[] |
允许格式(数组) |
.getUploadPath() |
String |
上传路径 |
用户配置 appProperties.getUser()
| 方法 |
返回类型 |
说明 |
.getMaxPasswordRetry() |
int |
密码最大重试次数 |
.getMaxLoginDevice() |
int |
最大同时登录设备数 |
.getLockMinutes() |
int |
账户锁定时间(分钟) |
CORS 配置 appProperties.getCors()
| 方法 |
返回类型 |
说明 |
.getAllowedOrigins() |
String |
允许的源(逗号分隔) |
.getAllowedOriginsArray() |
String[] |
允许的源(数组) |
7.5 使用示例
String secret = appProperties.getJwt().getSecret();
long expirationMs = appProperties.getJwt().getExpirationMs();
long maxSize = appProperties.getFile().getMaxSize();
if (file.getSize() > maxSize) {
throw new BusinessException("文件不能超过 " + appProperties.getFile().getMaxSizeMB() + "MB");
}
if (appProperties.isProduction()) {
}
String salt = appProperties.getSecurity().getPasswordSalt();
int strength = appProperties.getSecurity().getBcryptStrength();
String[] origins = appProperties.getCors().getAllowedOriginsArray();
7.6 文件清单
| 文件 |
用途 |
是否提交 Git |
application.yml |
主配置,所有默认值 |
✅ |
application-dev.yml |
开发环境覆盖 |
✅ |
application-prod.yml |
生产环境覆盖 |
✅ |
.env.example |
环境变量模板 |
✅ |
.env.dev |
开发环境变量 |
❌ |
.env.prod |
生产环境变量 |
❌ |
八、API文档规范
8.1 Controller 注解
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户相关接口")
public class UserController {
@GetMapping("/{id}")
@Operation(summary = "获取用户详情", description = "根据ID获取用户信息")
public Result<UserVO> getById(
@Parameter(description = "用户ID") @PathVariable Long id) {
}
}
8.2 DTO/VO 注解
@Data
@Schema(description = "用户注册请求")
public class UserRegisterDTO {
@Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description = "密码", example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
}
8.3 生产环境禁用
knife4j:
enable: ${KNIFE4J_ENABLE:false}
九、快速检查清单
开发前检查
- 是否定义了合适的 ErrorCode
- DTO 是否添加了校验注解
- 枚举是否包含
@EnumValue、@JsonValue、fromCode()
开发中检查
- Controller 是否只做参数接收和结果返回
- Service 是否添加了
@Transactional(写操作)
- 是否使用 ThrowUtils 进行参数校验
- 是否进行了数据级权限校验
提交前检查
- 敏感配置是否在
.env 文件中
.env 文件是否在 .gitignore 中
- API 文档注解是否完整
- 生产环境是否禁用了 Knife4j
十、启动与部署
10.1 环境文件说明
| 文件 |
用途 |
提交 Git |
.env.example |
环境变量模板,包含所有配置项 |
✅ |
.env.dev |
开发环境配置 |
❌ |
.env.prod |
生产服务器配置 |
❌ |
.env.prod.local |
本地模拟生产环境(连接测试库等) |
❌ |
首次使用:复制模板并填写配置
cp .env.example .env.dev
cp .env.example .env.prod
cp .env.example .env.prod.local
10.2 本地开发启动
Windows(推荐使用启动脚本)
项目根目录/
├── start-dev.bat # 开发环境启动
└── start-prod.bat # 本地模拟生产环境启动
双击运行 start-dev.bat 或在命令行:
.\start-dev.bat
IDEA 启动配置
- Edit Configurations → Add New → Spring Boot
- 配置如下:
| 配置项 |
值 |
| Main class |
com.zwnsyw.zwwwspringbootbasetemplate.ZwwwSpringBootBaseTemplateApplication |
| Active profiles |
dev |
| Environment variables |
从 .env.dev 复制,或使用 EnvFile 插件 |
手动设置环境变量(IDEA):
APP_JWT_SECRET=your-secret-key;APP_SECURITY_PASSWORD_SALT=your-salt;DB_PASSWORD=xxx
使用 EnvFile 插件(推荐):
- 安装插件:File → Settings → Plugins → 搜索 "EnvFile"
- Run Configuration → EnvFile 标签 → 勾选 Enable → 添加
.env.dev
Maven 命令启动
mvn spring-boot:run -Dspring-boot.run.profiles=dev
export $(cat .env.dev | grep -v '^#' | xargs) && mvn spring-boot:run -Dspring-boot.run.profiles=dev
10.3 服务器部署
10.3.1 打包
mvn clean package -DskipTests
target/zwww-springboot-base-template-0.0.1-SNAPSHOT.jar
10.3.2 上传部署文件
scp target/*.jar user@server:/opt/app/
scp .env.prod user@server:/opt/app/.env
scp deploy.sh user@server:/opt/app/
10.3.3 服务器目录结构
/opt/app/
├── zwww-springboot-base-template.jar # 应用 jar
├── .env # 环境变量文件
├── deploy.sh # 部署脚本
├── logs/ # 日志目录
│ ├── app.log # 应用日志
│ └── error.log # 错误日志
└── backup/ # 备份目录
10.3.4 部署脚本 deploy.sh
#!/bin/bash
APP_NAME="zwww-springboot-base-template"
APP_JAR="${APP_NAME}.jar"
APP_DIR="/opt/app"
LOG_DIR="${APP_DIR}/logs"
ENV_FILE="${APP_DIR}/.env"
PID_FILE="${APP_DIR}/${APP_NAME}.pid"
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"
mkdir -p ${LOG_DIR}
load_env() {
if [ -f "${ENV_FILE}" ]; then
echo "加载环境变量: ${ENV_FILE}"
export $(cat ${ENV_FILE} | grep -v '^#' | grep -v '^$' | xargs)
else
echo "[错误] 环境变量文件不存在: ${ENV_FILE}"
exit 1
fi
}
get_pid() {
if [ -f "${PID_FILE}" ]; then
cat ${PID_FILE}
else
echo ""
fi
}
is_running() {
local pid=$(get_pid)
if [ -n "${pid}" ] && ps -p ${pid} > /dev/null 2>&1; then
return 0
else
return 1
fi
}
start() {
if is_running; then
echo "[警告] ${APP_NAME} 已在运行中 (PID: $(get_pid))"
return 1
fi
load_env
echo "启动 ${APP_NAME}..."
cd ${APP_DIR}
nohup java ${JAVA_OPTS} \
-Dspring.profiles.active=prod \
-jar ${APP_JAR} \
> ${LOG_DIR}/app.log 2>&1 &
echo $! > ${PID_FILE}
sleep 3
if is_running; then
echo "[成功] ${APP_NAME} 已启动 (PID: $(get_pid))"
else
echo "[错误] ${APP_NAME} 启动失败,请检查日志"
cat ${LOG_DIR}/app.log | tail -50
return 1
fi
}
stop() {
if ! is_running; then
echo "[信息] ${APP_NAME} 未运行"
return 0
fi
local pid=$(get_pid)
echo "停止 ${APP_NAME} (PID: ${pid})..."
kill ${pid}
local count=0
while is_running && [ ${count} -lt 30 ]; do
sleep 1
count=$((count + 1))
echo -n "."
done
echo ""
if is_running; then
echo "[警告] 进程未响应,强制终止..."
kill -9 ${pid}
fi
rm -f ${PID_FILE}
echo "[成功] ${APP_NAME} 已停止"
}
restart() {
stop
sleep 2
start
}
status() {
if is_running; then
echo "[运行中] ${APP_NAME} (PID: $(get_pid))"
else
echo "[已停止] ${APP_NAME}"
fi
}
logs() {
tail -f ${LOG_DIR}/app.log
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
logs)
logs
;;
*)
echo "用法: $0 {start|stop|restart|status|logs}"
exit 1
;;
esac
10.3.5 部署命令速查
| 操作 |
命令 |
| 启动 |
./deploy.sh start |
| 停止 |
./deploy.sh stop |
| 重启 |
./deploy.sh restart |
| 状态 |
./deploy.sh status |
| 查看日志 |
./deploy.sh logs |
| 实时日志 |
tail -f /opt/app/logs/app.log |
10.4 Docker 部署
Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
构建与运行
docker build -t zwww-app:latest .
docker run -d \
--name zwww-app \
--env-file .env.prod \
-p 8080:8080 \
zwww-app:latest
docker logs -f zwww-app
10.5 常见问题
| 问题 |
原因 |
解决方案 |
| 环境变量未生效 |
未正确加载 .env 文件 |
使用启动脚本或 EnvFile 插件 |
| 端口被占用 |
8080 端口已使用 |
`netstat -ano |
| 启动后立即退出 |
配置错误 |
查看日志 logs/app.log |
| 数据库连接失败 |
配置或网络问题 |
检查 DB_HOST、DB_PORT、防火墙 |
附录:项目结构
src/main/java/com/zwnsyw/zwwwspringbootbasetemplate/
├── ZwwwSpringBootBaseTemplateApplication.java # 启动类
│
├── config/ # 配置类
│ ├── AppProperties.java # 应用配置属性
│ ├── MyBatisPlusConfig.java # MyBatis-Plus 配置
│ ├── Knife4jConfig.java # Knife4j API文档配置
│ └── CorsConfig.java # 跨域配置
│
├── controller/ # 控制器层
│ ├── UserController.java
│ └── ConfigController.java
│
├── service/ # 服务层接口
│ └── UserService.java
│
├── service/impl/ # 服务层实现
│ └── UserServiceImpl.java
│
├── mapper/ # MyBatis Mapper 接口
│ └── UserMapper.java
│
├── model/ # 模型层
│ ├── entity/ # 数据库实体
│ │ └── User.java
│ ├── dto/ # 数据传输对象(请求)
│ │ ├── UserDTO.java
│ │ └── user/
│ │ ├── UserRegisterDTO.java
│ │ ├── UserLoginDTO.java
│ │ └── UserUpdateDTO.java
│ ├── vo/ # 视图对象(响应)
│ │ └── UserVO.java
│ ├── query/ # 查询对象
│ │ └── UserQueryDTO.java
│ └── enums/ # 枚举类
│ ├── GenderEnum.java
│ ├── UserStatusEnum.java
│ └── UserRoleEnum.java
│
├── common/ # 公共模块
│ ├── BaseResponse.java # 统一响应体
│ ├── ErrorCode.java # 错误码枚举
│ ├── ResultUtils.java # 响应工具类
│ └── PageResult.java # 分页结果
│
├── exception/ # 异常处理
│ ├── BusinessException.java # 业务异常
│ └── GlobalExceptionHandler.java # 全局异常处理器
│
├── security/ # 安全模块
│ ├── annotation/ # 注解定义
│ │ ├── Anonymous.java # 匿名访问
│ │ ├── RequiresPermission.java # 权限校验
│ │ └── RequiresRole.java # 角色校验
│ ├── config/ # 安全配置
│ │ ├── SecurityConfig.java # 安全配置
│ │ └── AnonymousUrlConfig.java # 匿名URL配置
│ ├── context/ # 安全上下文
│ │ └── SecurityContext.java # 当前用户上下文
│ ├── enums/ # 枚举
│ │ └── Logical.java # 逻辑符枚举
│ ├── handler/ # 权限处理器
│ │ └── PermissionHandler.java # 权限校验逻辑
│ ├── interceptor/ # 拦截器
│ │ └── AuthorizationInterceptor.java # 鉴权拦截器
│ └── utils/ # 安全工具类
│ └── SecurityUtils.java
│
├── utils/ # 工具类
│ └── PasswordUtils.java # 密码工具类
│
└── validation/ # 自定义校验器
└── groups/
└── UpdateGroup.java # 更新分组
src/main/resources/
├── application.yml # 主配置文件
├── application-dev.yml # 开发环境配置
├── application-prod.yml # 生产环境配置
└── mapper/ # MyBatis XML 映射文件
└── UserMapper.xml
db/
└── init.sql # 数据库初始化脚本
# 环境变量文件与启动脚本(根目录)
项目根目录/
├── start-dev.bat # 开发环境启动
├── start-prod.bat # 本地模拟生产环境启动
├── .env.example # 环境变量模板(提交Git)
├── .env.dev # 开发环境变量(不提交)
├── .env.prod # 生产环境变量(不提交)
└── .env.prod.local # 本地模拟生产环境(不提交)
项目分区导航:⬅️ 00-最佳实践 | 01-Spring Boot MVC 项目最佳实践规范 | ➡️ 02-SpringBoot MVC 分层最佳实践
💬 评论